package com.aphrodite.ui.widgets.barcode;

import java.util.Hashtable;
import java.util.Vector;

import com.aphrodite.util.CharacterIterator;
import com.aphrodite.util.StringCharacterIterator;
import com.aphrodite.util.geom.Dimension;

/**
 * UPC-A barcode widget
 * 
 * 
 * @author kentelt
 * 
 */
class UPCABarcode extends LinearBarcode {
	/**
	 * Contains info on how to represent each block in a UPC barcode
	 * 
	 * @author kentelt
	 * 
	 */
	static class ModuleTemplate {

		private final static Module CENTRE_GUARD = new Module(new int[] { 0, 1, 1, 1, 1, 1 });
		private final static int GUARD_CHAR_SIZE = 1;
		private final static Vector KEYS_LEFT = new Vector();

		protected final static java.util.Vector KEYS_RIGHT = new Vector();
		/** The guards for the barcode */
		private final static Module LEFT_GUARD = new Module(new int[] { 1, 1, 1 });;

		private final static Module LEFT_MARGIN = new BlankModule(11);
		private final static int LEFT_WIDTH = 6;

		private final static Module RIGHT_GUARD = new Module(new int[] { 1, 1, 1 });
		private final static Module RIGHT_MARGIN = new BlankModule(11);

		private final static Hashtable SET_LEFT = new Hashtable();
		protected final static Hashtable SET_RIGHT = new Hashtable();

		static {
			initBaseSet();
		}

		/**
		 * Returns the module that represents the specified character.
		 * 
		 * @param key
		 *            The data character to get the encoding module for
		 * @param position
		 *            The position of the data character, starts at 0
		 * @return The module that encodes the given char
		 */
		protected static Module getModule(String key, int position) {
			Module module = null;
			if (position + 1 > LEFT_WIDTH) {
				module = (Module) SET_RIGHT.get(key);
			} else {
				module = (Module) SET_LEFT.get(key);
			}
			module.setSymbol(key);
			return module;
		}

		/**
		 * Returns the encoded module at the given index position. This is used
		 * to get the encoded checksum character.
		 * 
		 * @param index
		 *            The index of the module required
		 * @return The module at the specified index
		 */
		private static Module getModuleForIndex(int index) {
			if (index + 1 > LEFT_WIDTH) {
				return getModule((String) KEYS_RIGHT.elementAt(index), index);
			} else {
				return getModule((String) KEYS_LEFT.elementAt(index), index);
			}
		}

		/**
		 * Indicates whether the given key is represented in the default
		 * encoding table that this module factory contains.
		 * 
		 * @return True if the key has a direct module encoding, false if not
		 */
		private static boolean hasModule(String key) {
			if (KEYS_RIGHT.indexOf(key) > -1) {
				return true;
			}
			if (KEYS_LEFT.indexOf(key) > -1) {
				return true;
			}
			return false;
		}

		/*
	     *
	     */
		/**
		 * Initialise the module definitions.
		 */
		private static void initBaseSet() {
			initRightSet();
			initLeftSet();
		}

		private static void initLeftSet() {
			// left side
			KEYS_LEFT.addElement("0");
			SET_LEFT.put("0", new Module(new int[] { 0, 3, 2, 1, 1 }));
			KEYS_LEFT.addElement("1");
			SET_LEFT.put("1", new Module(new int[] { 0, 2, 2, 2, 1 }));
			KEYS_LEFT.addElement("2");
			SET_LEFT.put("2", new Module(new int[] { 0, 2, 1, 2, 2 }));
			KEYS_LEFT.addElement("3");
			SET_LEFT.put("3", new Module(new int[] { 0, 1, 4, 1, 1 }));
			KEYS_LEFT.addElement("4");
			SET_LEFT.put("4", new Module(new int[] { 0, 1, 1, 3, 2 }));
			KEYS_LEFT.addElement("5");
			SET_LEFT.put("5", new Module(new int[] { 0, 1, 2, 3, 1 }));
			KEYS_LEFT.addElement("6");
			SET_LEFT.put("6", new Module(new int[] { 0, 1, 1, 1, 4 }));
			KEYS_LEFT.addElement("7");
			SET_LEFT.put("7", new Module(new int[] { 0, 1, 3, 1, 2 }));
			KEYS_LEFT.addElement("8");
			SET_LEFT.put("8", new Module(new int[] { 0, 1, 2, 1, 3 }));
			KEYS_LEFT.addElement("9");
			SET_LEFT.put("9", new Module(new int[] { 0, 3, 1, 1, 2 }));
		}

		static void initRightSet() {
			// right side
			KEYS_RIGHT.addElement("0");
			SET_RIGHT.put("0", new Module(new int[] { 3, 2, 1, 1 }));
			KEYS_RIGHT.addElement("1");
			SET_RIGHT.put("1", new Module(new int[] { 2, 2, 2, 1 }));
			KEYS_RIGHT.addElement("2");
			SET_RIGHT.put("2", new Module(new int[] { 2, 1, 2, 2 }));
			KEYS_RIGHT.addElement("3");
			SET_RIGHT.put("3", new Module(new int[] { 1, 4, 1, 1 }));
			KEYS_RIGHT.addElement("4");
			SET_RIGHT.put("4", new Module(new int[] { 1, 1, 3, 2 }));
			KEYS_RIGHT.addElement("5");
			SET_RIGHT.put("5", new Module(new int[] { 1, 2, 3, 1 }));
			KEYS_RIGHT.addElement("6");
			SET_RIGHT.put("6", new Module(new int[] { 1, 1, 1, 4 }));
			KEYS_RIGHT.addElement("7");
			SET_RIGHT.put("7", new Module(new int[] { 1, 3, 1, 2 }));
			KEYS_RIGHT.addElement("8");
			SET_RIGHT.put("8", new Module(new int[] { 1, 2, 1, 3 }));
			KEYS_RIGHT.addElement("9");
			SET_RIGHT.put("9", new Module(new int[] { 3, 1, 1, 2 }));
		}

		/**
		 * Cannot construct.
		 */
		ModuleTemplate() {}

	}

	private final static int BARCODE_LENGTH = 12;

	private final static int CHECKSUM_WEIGHT_EVEN = 1;
	private final static int CHECKSUM_WEIGHT_ODD = 3;

	/**
	 * Indicates whether the given character is valid for this barcode or not.
	 * This basically just checks to see whether the key is in the list of
	 * encoded characters.
	 * 
	 * @param key
	 *            The key to check for validity
	 * @return True if the key is valid, false otherwise
	 */
	static boolean isValid(String key) {
		if (ModuleTemplate.KEYS_RIGHT.indexOf(key) > -1) {
			return true;
		}

		if (ModuleTemplate.KEYS_LEFT.indexOf(key) > -1) {
			return true;
		}

		return false;
	}

	private static String validateChars(String data) throws BarcodeException {
		if (data == null) {
			throw new IllegalArgumentException("Data cannot be null");
		}
		StringCharacterIterator iter = new StringCharacterIterator(data);
		for (char c = iter.first(); c != CharacterIterator.DONE; c = iter.next()) {
			if (!ModuleTemplate.hasModule(String.valueOf(c))) {
				System.out.println("UPC barcodes can only contain numbers.");
				throw new BarcodeException("Illegal character: " + c);
			}
		}
		return data;
	}

	final String label;

	// private static final String[] TYPES = new String[] { "UPC-A", "UPCA" };
	boolean requiresChecksum = false;

	int width = 0;

	UPCABarcode(String data, boolean randomWeight, int height) throws BarcodeException {
		super(validateChars(data), height);
		if (data.length() != BARCODE_LENGTH) {
			throw new BarcodeException("Invalid data length. UPC barcodes should be " + BARCODE_LENGTH + " digits long");
		}

		requiresChecksum = true;
		if (randomWeight) {
			data = populateRandonWeightCheckDigit(data);
		}
		this.label = data;
	}

	/**
	 * Returns the checksum for the barcode, pre-encoded as a Module.
	 * 
	 * @return a Mod-10 caclulated checksum, if no checksum is required Null
	 */
	Module calculateChecksum() {
		if (requiresChecksum) {
			return ModuleTemplate.getModuleForIndex(getMod10CheckDigit(data));
		}

		return null;
	}

	// protected int getBarcodeLength() {
	// return BARCODE_LENGTH;
	// }

	int calculateChecksum(int value, boolean even) {
		if (even) {
			return value * CHECKSUM_WEIGHT_EVEN;
		} else {
			return value * CHECKSUM_WEIGHT_ODD;
		}
	}

	public void cleanUp() {

	}

	/*
	 * TODO: The following is very close to EAN13Barcode's version, should
	 * change this so EAN13Barcode doesn't need to override.
	 * 
	 * Note that the following code uses member functions to get static values
	 * from the ModuleFactory class, instead of referencing the values directly.
	 * This is so sub-classes can override the member functions and thus change
	 * the static values.
	 */
	Dimension draw(Output output, int x, int y, int barWidth, int barHeight) throws OutputException {
		int currentX = x;
		output.beginDraw();
		// need to change the output.barHeight value, appears to be no means to
		// do so
		int guardBarHeight = 0;
		int shortBarHeight = barHeight;
		int textHeight = 10 * barWidth;

		if (drawingText) {
			shortBarHeight = barHeight - (11 * barWidth);
			guardBarHeight = shortBarHeight + (6 * barWidth);
		} else {
			shortBarHeight = barHeight - (6 * barWidth);
			guardBarHeight = barHeight;
		}

		// String text = getLabel();
		int currentY = this.height + y;

		Module[] modules = encodeData();

		// String leadChar = String.valueOf(text.charAt(0));
		// String endChar = String.valueOf(text.charAt(text.length() - 1));
		// String firstSet = text.substring(1, getLeftWidth());
		// String lastSet = text.substring(getLeftWidth(), 11);

		// if (requiresChecksum) {
		// endChar = calculateChecksum().getSymbol();
		// }

		int startTextPos = 0;
		int firstTextPos = 0;
		int secondTextPos = 0;
		int endTextPos = 0;

		int startTextW = x;
		int firstTextW = 0;
		int secondTextW = 0;
		int endTextW = 0;
		int width = 0;
		Module preAmble = getPreAmble();
		Module postAmble = getPostAmble();
		startTextW = 0;

		// draw leading white space
		if (super.drawingQuietSection) {
			currentX += drawModule(getLeftMargin(), output, currentX, y, barWidth, shortBarHeight + textHeight);
		}
		startTextPos = x;
		startTextW = currentX - startTextPos;
		width = currentX;
		int guardCharSize = getGuardCharSize();
		int leftWidth = getLeftWidth();

		// draw the left guard
		if (preAmble != null) {
			currentX += drawModule(preAmble, output, currentX, y, barWidth, guardBarHeight);
		}

		// draw first char in left side
		for (int i = 0; i < guardCharSize; i++) {
			currentX += drawModule(modules[0], output, currentX, y, barWidth, guardBarHeight);
		}
		firstTextPos = currentX;

		// draw the blank space below the guard
		width = currentX - width;
		output.paintBackground(currentX - width, guardBarHeight, width,
				((shortBarHeight + textHeight) - guardBarHeight));

		for (int i = guardCharSize; i < leftWidth; i++) {
			currentX += drawModule(modules[i], output, currentX, y, barWidth, shortBarHeight);
		}

		firstTextW = currentX - firstTextPos;

		width = currentX;
		// draw the centre guard
		currentX += drawModule(getCentreGuard(), output, currentX, y, barWidth, guardBarHeight);
		secondTextPos = currentX;

		// draw the blank space below the guard
		width = currentX - width;
		output.paintBackground(currentX - width, guardBarHeight, width,
				((shortBarHeight + textHeight) - guardBarHeight));

		int endGuardOffset = modules.length - guardCharSize;

		for (int i = leftWidth; i < endGuardOffset; i++) {
			currentX += drawModule(modules[i], output, currentX, y, barWidth, shortBarHeight);
		}

		secondTextW = currentX - secondTextPos;
		width = currentX;
		for (int i = endGuardOffset; i < modules.length; i++) {
			currentX += drawModule(modules[i], output, currentX, y, barWidth, guardBarHeight);
		}

		// draw the right guard
		if (postAmble != null) {
			currentX += drawModule(postAmble, output, currentX, y, barWidth, guardBarHeight);
		}

		// draw the blank space below the guard
		width = currentX - width;
		output.paintBackground(currentX - width, guardBarHeight, width,
				((shortBarHeight + textHeight) - guardBarHeight));

		endTextPos = currentX;

		// draw trailing white space
		if (super.drawingQuietSection) {
			currentX += drawModule(getRightMargin(), output, currentX, y, barWidth, shortBarHeight + textHeight);
		}

		endTextW = currentX - endTextPos;

		// if (drawingText) {
		// output.drawText(leadChar,
		// LabelLayoutFactory.createMarginLayout(startTextPos, shortBarHeight,
		// startTextW,
		// textHeight));
		// output.drawText(firstSet,
		// LabelLayoutFactory.createMarginLayout(firstTextPos, shortBarHeight,
		// firstTextW,
		// textHeight));
		// output.drawText(lastSet,
		// LabelLayoutFactory.createMarginLayout(secondTextPos, shortBarHeight,
		// secondTextW,
		// textHeight));
		// output.drawText(endChar,
		// LabelLayoutFactory.createMarginLayout(endTextPos, shortBarHeight,
		// endTextW,
		// textHeight));
		// }

		Dimension size = new Dimension((currentX - x), (currentY) - y);

		output.endDraw((int) size.getWidth(), (int) size.getHeight());

		return size;
	}

	/**
	 * Returns the encoded data for the barcode.
	 * 
	 * @return An array of modules that represent the data as a barcode
	 */
	Module[] encodeData() {
		Vector modules = new Vector();
		Module module = null;
		int len = data.length();
		char c;
		for (int i = 0; i < len; i++) {
			c = data.charAt(i);

			module = ModuleTemplate.getModule(String.valueOf(c), i);
			width += module.widthInBars();
			modules.addElement(module);
		}

		if (requiresChecksum) {
			module = ModuleTemplate.getModule(calculateChecksum().getSymbol(), modules.size() - 1);
			width += module.widthInBars();
			modules.addElement(module);
		}
		Module[] ret = new Module[modules.size()];
		modules.copyInto(ret);
		return ret;

	}

	/**
	 * Returns the barcode width for the given resolution.
	 * 
	 * @param resolution
	 *            The output resolution
	 * @return The barcode width
	 */
	double getBarcodeWidth(int resolution) {
		encodeData();

		return super.width * width;
	}

	/**
	 * Returns the middle bar for the barcode.
	 * 
	 * @return pre amble for the barcode
	 */
	Module getCentreGuard() {
		return ModuleTemplate.CENTRE_GUARD;
	}

	int getGuardCharSize() {
		return ModuleTemplate.GUARD_CHAR_SIZE;
	}

	/**
	 * Returns the text that will be displayed underneath the barcode (if
	 * requested).
	 * 
	 * @return The text label for the barcode
	 */
	public String getLabel() {
		return label;
	}

	Module getLeftMargin() {
		return ModuleTemplate.LEFT_MARGIN;

	}

	int getLeftWidth() {
		return ModuleTemplate.LEFT_WIDTH;
	}

	protected int getMod10CheckDigit(String data) {
		int sum = 0;
		int len = data.length();
		int value;

		/*
		 * note that the for loop is from 0, as indexing for data is from 0, but
		 * the modolo 10 considers the first character position to be 1. as such
		 * 0 is odd, not even and 1 is even not odd, so compare to 1, not 0 when
		 * attempting to find if its an even or odd number.
		 */
		for (int i = 0; i < len; i++) {
			try {
				value = Integer.parseInt(String.valueOf(data.charAt(i)));
				sum += calculateChecksum(value, (i % 2) == 1);
			} catch (java.lang.NumberFormatException e) {
			}
		}

		int checkDigit = 10 - (sum % 10);

		if (checkDigit == 10) {
			checkDigit = 0;
		}

		return checkDigit;
	}

	/**
	 * Returns the post-amble for the barcode.
	 * 
	 * @return postamble for the barcode
	 */
	Module getPostAmble() {
		return ModuleTemplate.RIGHT_GUARD;
	}

	/**
	 * Returns the pre-amble for the barcode.
	 * 
	 * @return pre amble for the barcode
	 */
	Module getPreAmble() {
		return ModuleTemplate.LEFT_GUARD;
	}

	Module getRightMargin() {
		return ModuleTemplate.RIGHT_MARGIN;
	}

	private String populateRandonWeightCheckDigit(String upc) {
		int[][] checkDigitCalcs = { { 0, 2, 4, 6, 8, 9, 1, 3, 5, 7 }, { 0, 3, 6, 9, 2, 5, 8, 1, 4, 7 },
				{ 0, 5, 9, 4, 8, 3, 7, 2, 6, 1 } };
		int total = 0;
		int checkdigit = 0;
		char[] upcCharArray = upc.toCharArray();

		int digit = Character.digit(upcCharArray[7], 10);
		total += checkDigitCalcs[0][digit];
		digit = Character.digit(upcCharArray[8], 10);
		total += checkDigitCalcs[0][digit];

		/*
		 * calculation changes for the 9th and 10th digits which is why we are
		 * not using a loop
		 */
		digit = Character.digit(upcCharArray[9], 10);
		total += checkDigitCalcs[1][digit];
		digit = Character.digit(upcCharArray[10], 10);
		total += checkDigitCalcs[2][digit];

		if ((total % 10) == 0) {
			checkdigit = 0;
		} else {
			checkdigit = (10 - (total % 10));
		}

		upcCharArray[6] = String.valueOf(checkdigit).charAt(0);
		return String.valueOf(upcCharArray);
	}

}
